﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Mono.Reflection;

namespace DelegateExpressionizer
{
    public static class DelegateExtensions
    {
        // ReSharper disable UnusedMember.Local
        private static Expression<TDelegate> DelegateLambda<TDelegate>(TDelegate delegat) where TDelegate : class
        // ReSharper restore UnusedMember.Local
        {
            return ToExpression<TDelegate>(delegat as Delegate);
        }

        public static Expression ToExpression(this Delegate @delegate)
        {
            // call generic DelegateLambda
            var method = typeof(DelegateExtensions).GetMethod("DelegateLambda", BindingFlags.NonPublic | BindingFlags.Static);
            var genericMethod = method.MakeGenericMethod(@delegate.GetType());
            return (Expression)genericMethod.Invoke(null, new[] { @delegate });
        }


        static Expression<TDelegate> ToExpression<TDelegate>(this Delegate @delegate)
        {
            var context = @delegate.Target;
            return ConvertMethod<TDelegate>(@delegate.Method);
        }

        internal static Expression<TDelegate> ConvertMethod<TDelegate>(MethodInfo method)
        {
            var paramDefs = method.GetParameters();
            var methodBody = method.GetMethodBody();
            if (methodBody != null)
            {
                var locals = methodBody.LocalVariables.OrderBy(lvi => lvi.LocalIndex).Select(lvi => Expression.Parameter(lvi.LocalType, "var" + lvi.LocalIndex)).ToArray();
                var parameters = ResolveParameters(method, paramDefs);
                var ex = InstructionParser.Create(locals, parameters).Process(method.GetInstructions().First(), method.ReturnType);
                return Expression.Lambda<TDelegate>(ex, parameters.Any() ? parameters.Except(new[] { parameters.First() }) : parameters);
            }
            return null;
        }

        internal static Expression ConvertMethod(MethodInfo method)
        {
            var paramDefs = method.GetParameters();
            var methodBody = method.GetMethodBody();
            if (methodBody != null)
            {
                var locals = methodBody.LocalVariables.OrderBy(lvi => lvi.LocalIndex).Select(lvi => Expression.Parameter(lvi.LocalType, "var" + lvi.LocalIndex)).ToArray();
                var parameters = ResolveParameters(method, paramDefs);
                var ex = InstructionParser.Create(locals, parameters).Process(method.GetInstructions().First(), method.ReturnType);
                return Expression.Lambda(ex, parameters);
            }
            return null;
        }

        private static ParameterExpression[] ResolveParameters(MethodInfo method, ParameterInfo[] parameters)
        {
            IList<ParameterExpression> args;
            Debug.Assert(method.DeclaringType != null);
            if (method.IsStatic)
                args = parameters
                    .Select(p => Expression.Parameter(p.ParameterType, p.Name))
                    .ToList();
            else
            {
                args = new[] { Expression.Parameter(method.DeclaringType, "this") }
                    .Union(parameters.Select(p => Expression.Parameter(p.ParameterType, p.Name)))
                    .ToList();
            }
            return args.ToArray();
        }
    }

}
